package com.zys.el.time.service.redis;

import com.alibaba.fastjson2.JSON;
import com.zys.el.time.constants.RedisConstants;
import com.zys.el.time.dto.ScrollPage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Component
public class CacheClient {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public <R, ID> R queryWithoutLock(String cacheKey, ID queryId, Class<R> type, Function<ID, R> dbFallback){
        return this.queryWithoutLock(cacheKey, queryId, type, dbFallback, RedisConstants.COMMON_TTL, RedisConstants.NULL_TTL, TimeUnit.MINUTES);
    }

    // 静态数据，多查几次也无所谓，多重写redis几次也行
    public <R, ID> R queryWithoutLock(String cacheKey, ID queryId, Class<R> type, Function<ID, R> dbFallback,
                                      Long expireTime, Long nullExpireTime, TimeUnit unit) {
        // 1.从redis查询缓存
        String dataStr = stringRedisTemplate.opsForValue().get(cacheKey);

        // 2.判断是否命中
        if (StringUtils.isNotBlank(dataStr)) {
            // 3.命中，直接返回
            return JSON.parseObject(dataStr, type);
        }

        // 判断是否为空字符串
        if ("".equals(dataStr)) {
            return null;
        }

        // 不存在，根据queryId查询数据库
        R r = dbFallback.apply(queryId);
        // r不存在，返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(cacheKey, "", nullExpireTime, TimeUnit.MINUTES);
            return null;
        }
        // r存在，写入redis
        stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(r), expireTime, unit);

        return r;
    }


    public <R extends ScrollDetail, T> ScrollPage<T> queryScrollPage(String cachePageKey, Map<String, Object> queryMap,
                                                                     Long max, int offset, int size,
                                                                     Function<Long, T> getVo,
                                                                     Function<Map<String, Object>, List<R>> dbFallback){
        return queryScrollPage(cachePageKey, queryMap,max,offset, size,
                getVo, dbFallback, RedisConstants.COMMON_TTL, TimeUnit.MINUTES);
    }

    /**
     * 滚动查询
     * @param cachePageKey 缓存key
     * @param queryMap 数据库查询所需的参数(id: 查询的id，必需; startIdx: 分页查询起始位置; size: 页大小)
     * @param max zset查询的最大值参数
     * @param offset  zset查询的偏移量参数
     * @param size 页大小
     * @param getVo 通过zset查询出来的id，拿去查询出实体类的方法
     * @param dbFallback 查询数据库分页的方法，queryMap将传回此方法
     * @param expireTime 分页的过期时间
     * @param unit 过期时间的单位
     * @param <R> 实现ScrollDetail接口的实体类对象
     * @param <T> 通过getVo方法查询出的对象
     * @return 滚动查询展示类，包含类型List<T>的数据
     */
    public <R extends ScrollDetail, T> ScrollPage<T> queryScrollPage(String cachePageKey, Map<String, Object> queryMap,
                                                               Long max, int offset, int size,
                                                               Function<Long, T> getVo,
                                                               Function<Map<String, Object>, List<R>> dbFallback,
                                                               Long expireTime, TimeUnit unit){
        Set<ZSetOperations.TypedTuple<String>> typedTuples =
                stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(cachePageKey, 0, max, offset, size);

        // 存在数据
        if (typedTuples != null && !typedTuples.isEmpty() && typedTuples.size() == size) {
            // 解析数据：blogId、minTime（时间戳）、offset
            ArrayList<Long> ids = new ArrayList<>();
            // 记录最小值
            long minTime = 0;
            // 记录最小值的数量
            int off = 1;
            for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
                // 获取id
                ids.add(Long.valueOf(tuple.getValue()));

                // 获取分数(时间戳)
                long time = tuple.getScore().longValue();

                if (time == minTime) {
                    off++;
                } else {
                    minTime = time;
                    off = 1;
                }
            }

            List<T> voList = ids.stream()
                    .map(getVo)
                    .collect(Collectors.toList());

            return new ScrollPage<>(voList, minTime, off);
        }


        log.debug("<==帖子zset不存在==>");

        Long startIdx = 0L;
        // 从已经有的开始查(zset数量就是page起始idx)
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(cachePageKey)) && typedTuples != null) {
            startIdx = stringRedisTemplate.opsForZSet().size(cachePageKey);
            if (!typedTuples.isEmpty() && typedTuples.size() < size) {
                startIdx -= typedTuples.size();
            }
        }

        // 查询数据库
        queryMap.put("startIdx", startIdx);
        queryMap.put("size", size);
        List<R> pageData = dbFallback.apply(queryMap);
        // 真的没有
        if (pageData == null || pageData.isEmpty()) {
            return new ScrollPage<>(Collections.emptyList(), -1L, -1);
        }

        // 记录最小值
        AtomicLong minTime = new AtomicLong();
        // 记录最小值的数量
        AtomicInteger off = new AtomicInteger(1);

        List<T> voList = pageData.stream().map(t -> {
            // 存入redis
            long milli = t.getScore();
            stringRedisTemplate.opsForZSet().add(cachePageKey, String.valueOf(t.getId()), milli);

            // 维护最小值和数量
            if (minTime.get() == milli) {
                off.getAndIncrement();
            } else {
                minTime.set(milli);
                off.set(1);
            }

            return getVo.apply(t.getId());
        }).collect(Collectors.toList());

        // 设置过期时间
        stringRedisTemplate.expire(cachePageKey, expireTime, unit);

        return new ScrollPage<>(voList, minTime.get(), off.get());
    }
}
